/*
 * $Id: serial.c,v 1.23 2005-12-28 11:53:24 bacon Exp $
 */

#include <xp/net/serial.h>
#include <xp/bas/memory.h>
#include <xp/bas/assert.h>
#include <xp/bas/sysapi.h>
#include <xp/bas/errno.h>

#ifndef ASCII_XON
	#define ASCII_XON 0x11
#endif
#ifndef ASCII_XOFF
	#define ASCII_XOFF 0x13
#endif

static xp_ssize_t __send (
	xp_serial_t* serial, const void* msg,
	xp_size_t len, xp_time_t timeout, xp_bool_t opt_all);

static xp_ssize_t __receive  (
	xp_serial_t* serial, void* buf,
	xp_size_t len, xp_time_t timeout, xp_bool_t opt_all);

xp_serial_t* xp_serial_open (
	xp_serial_t* serial, const xp_char_t* name, int baud_rate,
	int parity, int data_bits, int stop_bits, int flow_control)
{
#ifdef _WIN32
	DCB dcb;
	COMMTIMEOUTS ct;
#else
	struct termios attr;
#endif

	if (serial == XP_NULL) {
		serial = (xp_serial_t*)xp_malloc (xp_sizeof(xp_serial_t));
		if (serial == XP_NULL) return XP_NULL;
		serial->__dynamic = xp_true;
	}
	else serial->__dynamic = xp_false;

	xp_assert (
		parity == XP_SERIAL_PARITY_NONE ||
		parity == XP_SERIAL_PARITY_EVEN ||
		parity == XP_SERIAL_PARITY_ODD  ||
		parity == XP_SERIAL_PARITY_MARK ||
		parity == XP_SERIAL_PARITY_SPACE);

	xp_assert (
		stop_bits == XP_SERIAL_STOPBITS_ONE  ||
		stop_bits == XP_SERIAL_STOPBITS_ONE5 ||
		stop_bits == XP_SERIAL_STOPBITS_TWO);

#ifdef _WIN32
	/*
	 * FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING can be ORed
	 * with FILE_ATTRIBUTE_NORMAL to write directly to the port
	 * without intermediate cache.
	 */
	serial->handle = CreateFile (
		name, GENERIC_READ | GENERIC_WRITE,
		0, /* not shared */ XP_NULL, /* no security attributes */
		OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, XP_NULL);
	if (serial->handle == XP_SERIAL_INVALID_HANDLE) {
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}

	serial->__orgattr.DCBlength = xp_sizeof(serial->__orgattr);
	dcb.DCBlength = xp_sizeof(dcb);

	if (GetCommState (serial->handle, &serial->__orgattr) == FALSE) {
		CloseHandle (serial->handle);
		serial->handle = XP_SERIAL_INVALID_HANDLE;
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}

	if (GetCommState (serial->handle, &dcb) == FALSE) {
		CloseHandle (serial->handle);
		serial->handle = XP_SERIAL_INVALID_HANDLE;
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}
	dcb.BaudRate = baud_rate;
	dcb.ByteSize = data_bits;
	dcb.Parity   = parity;
	dcb.StopBits = stop_bits;

	dcb.XonChar = ASCII_XON;
	dcb.XoffChar = ASCII_XOFF;
	dcb.XonLim = 100;
	dcb.XoffLim = 100;
	dcb.fTXContinueOnXoff = TRUE; /* xoff continues tx */

	if (flow_control & XP_SERIAL_FLOWCONTROL_SOFTWARE) {
		dcb.fInX = TRUE;
		dcb.fOutX = TRUE;
	}
	else {
		dcb.fInX = FALSE;
		dcb.fOutX = FALSE;
	}

	if (flow_control & XP_SERIAL_FLOWCONTROL_HARDWARE)
		dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
	else dcb.fRtsControl = RTS_CONTROL_DISABLE;

	dcb.fOutxCtsFlow = FALSE; /* no cts output flow control */
	dcb.fOutxDsrFlow = FALSE; /* no dsr output flow control */
	dcb.fDtrControl = DTR_CONTROL_DISABLE; 
	dcb.fDsrSensitivity = FALSE;
	dcb.fErrorChar = FALSE; /* disable error replacement */
	dcb.fNull = FALSE; /* disable null stripping */
	dcb.fAbortOnError = FALSE; /* don't abort read/writes on error */

	if (SetCommState (serial->handle, &dcb) == FALSE) {
		CloseHandle (serial->handle);
		serial->handle = XP_SERIAL_INVALID_HANDLE;
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}

	/* disable timeouts */
	ct.ReadIntervalTimeout = 0;
	ct.ReadTotalTimeoutMultiplier = 0;
	ct.ReadTotalTimeoutConstant = 0;
	ct.WriteTotalTimeoutMultiplier = 0;
	ct.WriteTotalTimeoutConstant = 0;
	if (SetCommTimeouts (serial->handle, &ct) == FALSE) {
		CloseHandle (serial->handle);
		serial->handle = XP_SERIAL_INVALID_HANDLE;
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}
#else
	serial->handle = xp_open (name, O_RDWR | O_NOCTTY | O_NDELAY);
	if (serial->handle == XP_SERIAL_INVALID_HANDLE) {
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}

	if (tcgetattr (serial->handle, &serial->__orgattr) == -1) {
		xp_close (serial->handle);
		serial->handle = XP_SERIAL_INVALID_HANDLE;
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}

	xp_memcpy (&attr, &serial->__orgattr, sizeof(attr));

#if defined(sun) || defined(__sun)
	#define cfmakeraw(termios_p) { \
		(termios_p)->c_iflag &= \
			~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); \
		(termios_p)->c_oflag &= ~OPOST; \
		(termios_p)->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); \
		(termios_p)->c_cflag &= ~(CSIZE|PARENB); \
		(termios_p)->c_cflag |= CS8; \
	}
#endif
	cfmakeraw (&attr);

	attr.c_cc[VMIN] = 1;
	attr.c_cc[VTIME] = 0;

	baud_rate =
		(baud_rate == 300)?    B300:
		(baud_rate == 600)?    B600:
		(baud_rate == 1200)?   B1200:
		(baud_rate == 2400)?   B2400:
		(baud_rate == 4800)?   B4800:
		(baud_rate == 9600)?   B9600:
		(baud_rate == 19200)?  B19200:
		(baud_rate == 38400)?  B38400:
		(baud_rate == 57600)?  B57600:
		(baud_rate == 115200)? B115200:
		(baud_rate == 230400)? B230400: B9600;

	cfsetospeed (&attr, baud_rate);
	cfsetispeed (&attr, baud_rate);

	xp_assert (
		data_bits == 5 || data_bits == 6 ||
		data_bits == 7 || data_bits == 8);

	if (data_bits == 7 && 
	    (parity == XP_SERIAL_PARITY_MARK ||
	     parity == XP_SERIAL_PARITY_SPACE)) data_bits = 8;

	attr.c_cflag = (attr.c_cflag & ~CSIZE) | 
		((data_bits == 5)? CS5:
		 (data_bits == 6)? CS6:
		 (data_bits == 7)? CS7: CS8);
	
	attr.c_cflag &= ~(PARENB | PARODD);
	if (parity == XP_SERIAL_PARITY_EVEN) {
		attr.c_cflag |= PARENB; /* enable parity, default is even */
		attr.c_iflag |= INPCK;  /* enable input parity checking */
	}
	else if (parity == XP_SERIAL_PARITY_ODD) {
		attr.c_cflag |= (PARENB | PARODD); /* enable parity, set to odd */
		attr.c_iflag |= INPCK; /*enable input parity checking */
	}
	else {
		attr.c_cflag &= ~(PARENB); /* clear parity enable */
		attr.c_iflag &= ~(INPCK); /* disable input parity checking */
	}
	
	if (stop_bits == XP_SERIAL_STOPBITS_TWO) attr.c_cflag |= CSTOPB;
	else attr.c_cflag &= ~CSTOPB;

	if (flow_control & XP_SERIAL_FLOWCONTROL_HARDWARE)
		attr.c_cflag |= CRTSCTS; /* enable rts/cts flow  */
	else attr.c_cflag &= ~CRTSCTS; /* disable rts/cts flow */

	// TODO: refer to enableXonoff
	if (flow_control & XP_SERIAL_FLOWCONTROL_SOFTWARE)
		attr.c_iflag  |= IXON | IXOFF;
	else attr.c_iflag  &= ~(IXON | IXOFF | IXANY);
	
	tcflush (serial->handle, TCIFLUSH);
	if (tcsetattr (serial->handle, TCSANOW, &attr) == -1) {
		xp_close (serial->handle);
		serial->handle = XP_SERIAL_INVALID_HANDLE;
		if (serial->__dynamic) xp_free (serial);
		return XP_NULL;
	}
#endif

	return serial;
}

int xp_serial_close (xp_serial_t* serial)
{
	xp_assert (serial != XP_NULL);

#ifdef _WIN32
	SetCommState (serial->handle, &serial->__orgattr);
	if (CloseHandle (serial->handle) == FALSE) return -1;
#else
	tcsetattr (serial->handle, TCSANOW, &serial->__orgattr);
	if (xp_close (serial->handle) == -1) return -1;
#endif

	serial->handle = XP_SERIAL_INVALID_HANDLE;
	if (serial->__dynamic) xp_free (serial);
	return 0;
}

xp_ssize_t xp_serial_send (
	xp_serial_t* serial, const void* msg, xp_size_t len, xp_time_t timeout)
{
	xp_assert (serial != XP_NULL);
	return __send (serial, msg, len, timeout, xp_false);
}

xp_ssize_t xp_serial_sendall (
	xp_serial_t* serial, const void* msg, xp_size_t len, xp_time_t timeout)
{
	xp_assert (serial != XP_NULL);
	return __send (serial, msg, len, timeout, xp_true);
}

xp_ssize_t xp_serial_recv (
	xp_serial_t* serial, void* buf, xp_size_t len, xp_time_t timeout)
{
	xp_assert (serial != XP_NULL);
	return __receive (serial, buf, len, timeout, xp_false);
}

xp_ssize_t xp_serial_recvall (
	xp_serial_t* serial, void* buf, xp_size_t len, xp_time_t timeout)
{
	xp_assert (serial != XP_NULL);
	return __receive (serial, buf, len, timeout, xp_true);
}

static xp_ssize_t __send (
	xp_serial_t* serial, const void* msg,
	xp_size_t len, xp_time_t timeout, xp_bool_t opt_all)
{
	xp_size_t yet = len;
	const xp_byte_t* p = (const xp_byte_t*)msg;

#ifdef _WIN32
	DWORD nn, cerr;
	COMSTAT cstat;
	COMMTIMEOUTS ct;

	if (ClearCommError (serial->handle, &cerr, &cstat) == FALSE) return -1;

	if (GetCommTimeouts (serial->handle, &ct) == FALSE) return -1;
	if (ct.WriteTotalTimeoutMultiplier != 0 ||
	    ct.WriteTotalTimeoutConstant != timeout) {
		ct.WriteTotalTimeoutMultiplier = 0;
		ct.WriteTotalTimeoutConstant = timeout;
		if (SetCommTimeouts (serial->handle, &ct) == FALSE) return -1;
	}

	while (yet > 0) {
		if (WriteFile (serial->handle, p, yet, &nn, XP_NULL) == FALSE) return -1;
		if (nn == 0) break;
		yet -= nn; p += nn;

		if (!opt_all) break;
	}
#else
	int n;
	xp_ssize_t nn;

	while (yet > 0) {
		if (timeout > 0) {
			xp_poll_t p = { serial->handle, XP_POLLOUT };
 
			do {
				n = xp_poll (&p, 1, timeout);
			} while (n == -1 && xp_errno == XP_EINTR);
 
			if (n == -1) return -1;
			if (n == 0) {
				xp_seterrno (XP_ETIMEDOUT);
				return -1;
			}

			if ((p.revents & XP_POLLOUT) == 0) {
				xp_seterrno (XP_EINVAL);
				return -1;
			}
		}

		do {
			nn = write (serial->handle, p, yet);
		} while (nn == -1 && xp_errno == XP_EINTR);
 
		if (nn == -1) return -1;
		if (nn == 0) break;
 
		yet -= nn; p += nn;
		if (!opt_all) break;
	}
#endif

	return len - yet;
}

static xp_ssize_t __receive  (
	xp_serial_t* serial, void* buf,
	xp_size_t len, xp_time_t timeout, xp_bool_t opt_all)
{
	xp_size_t yet = len;
	xp_byte_t* p = (xp_byte_t*)buf;

#ifdef _WIN32
	DWORD nn, cerr;
	COMSTAT cstat;
	COMMTIMEOUTS ct;

	if (ClearCommError (serial->handle, &cerr, &cstat) == FALSE) return -1;

	if (GetCommTimeouts (serial->handle, &ct) == FALSE) return -1;
	if (ct.ReadIntervalTimeout != 0 ||
	    ct.ReadTotalTimeoutMultiplier != 0 ||
	    ct.ReadTotalTimeoutConstant != timeout) {
		ct.ReadIntervalTimeout = 0;
		ct.ReadTotalTimeoutMultiplier = 0;
		ct.ReadTotalTimeoutConstant = timeout;
		if (SetCommTimeouts (serial->handle, &ct) == FALSE) return -1;
	}

	while (yet > 0) {
		if (ReadFile (serial->handle, p, yet, &nn, 0) == FALSE) return -1;
		if (nn == 0) break;
		yet -= nn; p += nn;

		if (!opt_all) break;
	}
#else
	int n;
	xp_ssize_t nn;

	while (yet > 0) {
		if (timeout > 0) {
			xp_poll_t p = { serial->handle, XP_POLLIN };

			do {
				n = xp_poll (&p, 1, timeout);
			} while (n == -1 && xp_errno == XP_EINTR);

			if (n == -1) return -1;
			if (n == 0) {
				xp_seterrno (XP_ETIMEDOUT);
				return -1;
			}
			if ((p.revents & XP_POLLIN) == 0) {
				xp_seterrno (XP_EINVAL);
				return -1;
			}
		}

		do {
			nn = read (serial->handle, p, yet);
		} while (nn == -1 && xp_errno == XP_EINTR);

		if (nn == -1) return -1;
		if (nn == 0) break; // the peer connection is closed

		yet -= nn; p += nn;
		if (!opt_all) break;
	}
#endif

	return len - yet;
}

int xp_serial_drain (xp_serial_t* serial)
{
	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (FlushFileBuffers (serial->handle) == FALSE)? -1: 0;
#else
	return tcdrain (serial->handle);
#endif
}

int xp_serial_flush_input (xp_serial_t* serial)
{
	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (PurgeComm (serial->handle, 
		PURGE_RXABORT | PURGE_RXCLEAR) == FALSE)? -1: 0;
#else
	return tcflush (serial->handle, TCIFLUSH);
#endif
}

int xp_serial_flush_output (xp_serial_t* serial)
{
	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (PurgeComm (serial->handle, 
		PURGE_TXABORT | PURGE_TXCLEAR) == FALSE)? -1: 0;
#else
	return tcflush (serial->handle, TCOFLUSH);
#endif
}

int xp_serial_break (xp_serial_t* serial, xp_time_t duration)
{
	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	if (SetCommBreak (serial->handle) == FALSE) return -1;
	if (duration < 250) duration = 250;
	xp_sleep (duration);
	if (ClearCommBreak (serial->handle) == FALSE) return -1;
	return 0;
#else
	return tcsendbreak (serial->handle, duration / 250);
#endif
}

int xp_serial_get_status (xp_serial_t* serial, int* status)
{
#ifdef _WIN32
	DWORD st, st2 = 0;
#else
	int st, st2 = 0;
#endif

	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	if (GetCommModemStatus (serial->handle, &st) == FALSE) return -1;
	//if (st & MS_DTR_ON) st2 |= XP_SERIAL_DTR;
	//if (st & MS_RTS_ON) st2 |= XP_SERIAL_RTS;
	if (st & MS_DSR_ON) st2 |= XP_SERIAL_DSR;
	if (st & MS_CTS_ON) st2 |= XP_SERIAL_CTS;
#else
	if (ioctl (serial->handle, TIOCMGET, &st) == -1) return -1;
	if (st & TIOCM_DTR) st2 |= XP_SERIAL_DTR;
	if (st & TIOCM_RTS) st2 |= XP_SERIAL_RTS;
	if (st & TIOCM_DSR) st2 |= XP_SERIAL_DSR;
	if (st & TIOCM_CTS) st2 |= XP_SERIAL_CTS;
#endif

	*status = st2;
	return 0;
}

int xp_serial_set_status (xp_serial_t* serial, int status)
{
#ifdef _WIN32
	/*DWORD st;*/
#else
	int st;
#endif

	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	/*if (GetCommModemStatus (serial->handle, &st) == FALSE) return -1;
	if (status & XP_SERIAL_DTR) st |= MS_DTR_ON;
	if (status & XP_SERIAL_RTS) st |= MS_RTS_ON;
	if (status & XP_SERIAL_DSR) st |= MS_DSR_ON;
	if (status & XP_SERIAL_CTS) st |= MS_CTS_ON;
	return (SetCommModemStatus (serial->handle, &st) == FALSE)? -1: 0;*/
	return 0;
#else
	if (ioctl (serial->handle, TIOCMGET, &st) == -1) return -1;
	if (status & XP_SERIAL_DTR) st |= TIOCM_DTR;
	if (status & XP_SERIAL_RTS) st |= TIOCM_RTS;
	if (status & XP_SERIAL_DSR) st |= TIOCM_DSR;
	if (status & XP_SERIAL_CTS) st |= TIOCM_CTS;
	return ioctl (serial->handle, TIOCMSET, &st);
#endif
}

/*
int xp_serial_setdtr (xp_serial_t* serial)
{
#ifndef _WIN32
	int cmd = TIOCM_DTR;
#endif

	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (EscapeCommFunction (serial->handle, SETDTR) == FALSE)? -1: 0;
#else
	cmd = TIOCM_DTR;
	return ioctl (serial->handle, TIOCMBIS, &cmd);
#endif
}

int xp_serial_clrdtr (xp_serial_t* serial)
{
#ifndef _WIN32
	int cmd = TIOCM_DTR;
#endif

	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (EscapeCommFunction (serial->handle, CLRDTR) == FALSE)? -1: 0;
#else
	return ioctl (serial->handle, TIOCMBIC, &cmd);
#endif
}

int xp_serial_setrts (xp_serial_t* serial)
{
#ifndef _WIN32
	int cmd = TIOCM_RTS;
#endif

	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (EscapeCommFunction (serial->handle, SETRTS) == FALSE)? -1: 0;
#else
	return ioctl (serial->handle, TIOCMBIS, &cmd);
#endif
}

int xp_serial_clrrts (xp_serial_t* serial)
{
#ifndef _WIN32
	int cmd = TIOCM_RTS;
#endif

	xp_assert (serial != XP_NULL && 
		serial->handle != XP_SERIAL_INVALID_HANDLE);

#ifdef _WIN32
	return (EscapeCommFunction (serial->handle, CLRRTS) == FALSE)? -1: 0;
#else
	return ioctl (serial->handle, TIOCMBIC, &cmd);
#endif
}
*/
